跳到主要内容

Go 使用 TLS 协议-请求的原理

转载自 从Go的http.Client理解TLS过程 - 随便写点什么的文章 - 知乎

TLS 即 Transport Layer Security 传输层安全协议,它的前身是 SSL,即 Security Socket Layer 安全套接字协议。它工作在 OSI 七层模型中的表示层:

TLS协议可分为两部分:

记录协议:负责在传输连接上交换的所有底层消息,并可以配置加密。(对应表示层) 握手协议:负责协商连接参数,完成客户端服务器身份验证。(对应会话层)

如图是一次完整的 TLS 握手过程:

在握手过程中,客户端和服务器互相进行身份认证。

  1. 客户端发起握手请求,携带自己支持的协议以及版本加密方式等参数。
  2. 服务器选择连接参数并告诉客户端。
  3. 服务器发送自己的证书给客户端。
  4. 根据选择的密钥交换方式,服务器发送生成主密钥的额外信息。
  5. 服务器请求客户端证书。
  6. 服务器完成sayhello过程。
  7. 客户端发送证书给服务器。
  8. 客户端发送生成主密钥所需的额外信息。
  9. 验证证书。
  10. 客户端切换加密方式并通知服务器。
  11. 客户端完成握手。
  12. 服务器切换加密方式并通知客户端。
  13. 服务器握手完成。

在最终握手完成之后,双方便基于TLS握手中协商好的加密方式、应用层协议、加密密钥进行对称加密的方式进行消息传输。

Golang 中 HttpClient 源码实现 TLS 握手

在项目中,我们使用了 golang 默认的 http.DefaultClient 做为向第三方服务发起请求的 http 客户端,其中很重要的一个参数 Transport 使用的同样也是默认实现如下:

var DefaultTransport RoundTripper = &Transport{
Proxy: ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}

通过跟踪代码可以看到 http.Client 的 send(req *Request, deadline time.Time) 函数,最终是调用 Transport 的 roundTrip(req *Request) 发起的请求,在第一次进行请求时,roundTrip 会去创建一个新连接,局部代码如下:

// Get the cached or newly-created connection to either the
// host (for http or https), the http proxy, or the http proxy
// pre-CONNECTed to https server. In any case, we'll be ready
// to send it requests.
pconn, err := t.getConn(treq, cm)
if err != nil {
t.setReqCanceler(req, nil)
req.closeBody()
return nil, err
}

在 getConn 中便会真正的去创建连接,当判定到请求的协议为 https 协议时,便会进行TLS握手,可以看到代码实现是:

go func() {
if trace != nil && trace.TLSHandshakeStart != nil {
trace.TLSHandshakeStart()
}
err := tlsConn.Handshake()
if timer != nil {
timer.Stop()
}
errc <- err
}()

我们可以看到客户端hello实体和服务器回复hello实体:

hello := &clientHelloMsg{
vers: clientHelloVersion,
compressionMethods: []uint8{compressionNone},
random: make([]byte, 32),
sessionId: make([]byte, 32),
ocspStapling: true,
scts: true,
serverName: hostnameInSNI(config.ServerName),
supportedCurves: config.curvePreferences(),
supportedPoints: []uint8{pointFormatUncompressed},
nextProtoNeg: len(config.NextProtos) > 0,
secureRenegotiationSupported: true,
alpnProtocols: config.NextProtos,
supportedVersions: supportedVersions,
}

type serverHelloMsg struct {
raw []byte
vers uint16
random []byte
sessionId []byte
cipherSuite uint16
compressionMethod uint8
nextProtoNeg bool
nextProtos []string
ocspStapling bool
ticketSupported bool
secureRenegotiationSupported bool
secureRenegotiation []byte
alpnProtocol string
scts [][]byte
supportedVersion uint16
serverShare keyShare
selectedIdentityPresent bool
selectedIdentity uint16

// HelloRetryRequest extensions
cookie []byte
selectedGroup CurveID
}

携带的参数便是 TLS 握手过程需要用到的关键参数,如 alpnProtocols 中会包含 h2 和 http/1.1,这里的 h2 即为 http/2.0 协议,当服务器收到这个 hello 请求,便会从客户端的 hello 中挑选服务器支持的协议,如果服务器支持 h2 协议,那么在 TLS 握手完成之后 http 请求便会升级为 http/2.0。

接下来便是互相进行身份认证的过程:

// Does the handshake, either a full one or resumes old session. Requires hs.c,
// hs.hello, hs.serverHello, and, optionally, hs.session to be set.
func (hs *clientHandshakeState) handshake() error {
c := hs.c

isResume, err := hs.processServerHello()
if err != nil {
return err
}

hs.finishedHash = newFinishedHash(c.vers, hs.suite)

// No signatures of the handshake are needed in a resumption.
// Otherwise, in a full handshake, if we don't have any certificates
// configured then we will never send a CertificateVerify message and
// thus no signatures are needed in that case either.
if isResume || (len(c.config.Certificates) == 0 && c.config.GetClientCertificate == nil) {
hs.finishedHash.discardHandshakeBuffer()
}

hs.finishedHash.Write(hs.hello.marshal())
hs.finishedHash.Write(hs.serverHello.marshal())

c.buffering = true
if isResume {
if err := hs.establishKeys(); err != nil {
return err
}
if err := hs.readSessionTicket(); err != nil {
return err
}
if err := hs.readFinished(c.serverFinished[:]); err != nil {
return err
}
c.clientFinishedIsFirst = false
if err := hs.sendFinished(c.clientFinished[:]); err != nil {
return err
}
if _, err := c.flush(); err != nil {
return err
}
} else {
if err := hs.doFullHandshake(); err != nil {
return err
}
if err := hs.establishKeys(); err != nil {
return err
}
if err := hs.sendFinished(c.clientFinished[:]); err != nil {
return err
}
if _, err := c.flush(); err != nil {
return err
}
c.clientFinishedIsFirst = true
if err := hs.readSessionTicket(); err != nil {
return err
}
if err := hs.readFinished(c.serverFinished[:]); err != nil {
return err
}
}

c.ekm = ekmFromMasterSecret(c.vers, hs.suite, hs.masterSecret, hs.hello.random, hs.serverHello.random)
c.didResume = isResume
atomic.StoreUint32(&c.handshakeStatus, 1)

return nil
}

这里的过程便对应了上文提到的客户端服务器互相认证的过程。

当完成 TLS 握手认证之后,http.Client 将会缓存本条连接,并以最终协商的 http/2.0 协议进行通信,实现多路复用,所以可以确定的是这里的连接数始终是 1 条,并且提升到了 http/2.0 进行通信,http/2.0 提供的多路复用大大提高的请求效率。

References

SSL协议到底工作在OSI模型中的那一层?